/**
 * Copyright 2020-2023 the original author or Linlan authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.linlan.commons.core;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Array class to provide utils for use.
 * <p>
 * This class contains various utility methods for array operations such as checking if an array is empty or null,
 * resizing arrays, adding elements to arrays, creating new arrays, and more.
 *
 * @version 1.0
 * @since 1.0
 */
public final class ArrayUtils {

    /**
     * The value of index not found, which is -1.
     */
    public static final int INDEX_NOT_FOUND = -1;

    /**
     * An empty immutable {@code String} array.
     */
    public static final String[] EMPTY_STRING_ARRAY = new String[0];

    /**
     * An empty immutable {@code byte} array.
     */
    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    /**
     * An empty immutable {@code Throwable} array.
     *
     * @since 3.10
     */
    public static final Throwable[] EMPTY_THROWABLE_ARRAY = new Throwable[0];

    /**
     * An empty immutable {@code Field} array.
     *
     * @since 3.10
     */
    public static final Field[] EMPTY_FIELD_ARRAY = new Field[0];

    /**
     * Private constructor to prevent instantiation of this utility class.
     */
    private ArrayUtils() {
    }

    /**
     * Checks if the given source array is empty.
     *
     * @param source the source array to check
     * @param <T> 泛型包装
     * @return {@code true} if the source array is null or has a length of 0, {@code false} otherwise
     */
    public static <T> boolean isEmpty(final T... source) {
        return source == null || source.length == 0;
    }

    /**
     * Checks if the given source array is not empty.
     *
     * @param source the source array to check
     * @param <T> 泛型包装
     * @return {@code true} if the source array is not null and has a non-zero length, {@code false} otherwise
     */
    public static <T> boolean isNotEmpty(final T... source) {
        return !isEmpty(source);
    }

    /**
     * Checks if the given source array contains any null elements.
     *
     * @param source the source array to check
     * @param <T> 泛型包装
     * @return {@code true} if the source array contains at least one null element, {@code false} otherwise
     */
    public static <T> boolean hasNull(T... source) {
        if (isNotEmpty(source)) {
            for (T element : source) {
                if (null == element) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns the first non-null element from the given source array.
     *
     * @param source the source array to search
     * @param <T> 泛型包装
     * @return the first non-null element in the source array, or {@code null} if all elements are null or the array is empty
     */
    public static <T> T firstNotNull(T... source) {
        if (isNotEmpty(source)) {
            for (final T val : source) {
                if (null != val) {
                    return val;
                }
            }
        }
        return null;
    }

    /**
     * Creates a new array of the specified component type with the given size.
     *
     * @param componentType the component type of the new array
     * @param newSize       the size of the new array
     * @param <T> 泛型包装
     * @return a new array of the specified component type and size
     */
    public static <T> T[] newArray(Class<?> componentType, int newSize) {
        return (T[]) Array.newInstance(componentType, newSize);
    }

    /**
     * Forces the given source object to be cast to an array.
     * <p>
     * This method creates a new array with the specified type and copies the elements from the source object.
     *
     * @param type   the type of the source array or component type
     * @param source the source object to cast to an array
     * @return an array containing the elements of the source object
     * @throws NullPointerException     if the source object is null
     * @throws IllegalArgumentException if the source object is not an array
     */
    public static Object[] cast(Class<?> type, Object source) throws NullPointerException, IllegalArgumentException {
        if (null == source) {
            throw new NullPointerException("Argument [sourceObj] is null !");
        }
        if (false == source.getClass().isArray()) {
            throw new IllegalArgumentException("Argument [sourceObj] is not an array!");
        }
        if (null == type) {
            return (Object[]) source;
        }

        final Class<?> componentType = type.isArray() ? type.getComponentType() : type;
        final Object[] src = (Object[]) source;
        final Object[] dest = ArrayUtils.newArray(componentType, src.length);
        System.arraycopy(src, 0, dest, 0, src.length);
        return dest;
    }

    /**
     * Appends new elements to the given source array.
     * <p>
     * This method creates a new array that contains all the elements of the source array followed by the new elements.
     * The original source array is not modified.
     *
     * @param source      the source array
     * @param newElements the new elements to append
     * @param <T> 泛型包装
     * @return a new array containing the elements of the source array followed by the new elements
     */
    public static <T> T[] append(T[] source, T... newElements) {
        if (isEmpty(newElements)) {
            return source;
        }

        T[] t = resize(source, source.length + newElements.length);
        System.arraycopy(newElements, 0, t, source.length, newElements.length);
        return t;
    }

    /**
     * Resizes the given source array to the specified new size using the same component type as the source array.
     *
     * @param source  the source array to resize
     * @param newSize the new size of the array
     * @param <T> 泛型包装
     * @return a new array with the specified size and the same component type as the source array
     */
    public static <T> T[] resize(T[] source, int newSize) {
        return resize(source, newSize, source.getClass().getComponentType());
    }

    /**
     * Resizes the given source array to the specified new size using the given component type.
     *
     * @param source        the source array to resize
     * @param newSize       the new size of the array
     * @param componentType the component type of the new array
     * @param <T> 泛型包装
     * @return a new array with the specified size and component type
     */
    public static <T> T[] resize(T[] source, int newSize, Class<?> componentType) {
        T[] newArray = newArray(componentType, newSize);
        if (isNotEmpty(source)) {
            System.arraycopy(source, 0, newArray, 0, Math.min(source.length, newSize));
        }
        return newArray;
    }

    /**
     * Adds all the elements from the given source arrays into a new array.
     * <p>
     * Null arrays are ignored.
     *
     * @param sources the source arrays to add
     * @param <T> 泛型包装
     * @return a new array containing all the elements from the non-null source arrays
     */
    public static <T> T[] addAll(T[]... sources) {
        if (sources.length == 1) {
            return sources[0];
        }

        int length = 0;
        for (T[] source : sources) {
            if (source == null) {
                continue;
            }
            length += source.length;
        }
        T[] result = newArray(sources.getClass().getComponentType().getComponentType(), length);

        length = 0;
        for (T[] source : sources) {
            if (source == null) {
                continue;
            }
            System.arraycopy(source, 0, result, length, source.length);
            length += source.length;
        }
        return result;
    }

    /**
     * Copies a portion of the source array to the destination array.
     *
     * @param source  the source array
     * @param srcPos  the starting position in the source array
     * @param dest    the destination array
     * @param destPos the starting position in the destination array
     * @param length  the number of elements to copy
     * @return the destination array after the copy operation
     */
    public static Object copy(Object source, int srcPos, Object dest, int destPos, int length) {
        System.arraycopy(source, srcPos, dest, destPos, length);
        return dest;
    }

    /**
     * Copies a portion of the source array to the destination array starting from position 0 in both arrays.
     *
     * @param source the source array
     * @param dest   the destination array
     * @param length the number of elements to copy
     * @return the destination array after the copy operation
     */
    public static Object copy(Object source, Object dest, int length) {
        System.arraycopy(source, 0, dest, 0, length);
        return dest;
    }

    /**
     * Clones the given source array.
     *
     * @param source the source array to clone
     * @param <T> 泛型包装
     * @return a new array that is a clone of the source array, or {@code null} if the source array is null
     */
    public static <T> T[] clone(T[] source) {
        if (source == null) {
            return null;
        }
        return source.clone();
    }

    /**
     * Clones the given source object. If the source object is an array, a new array with the same elements is returned.
     *
     * @param source the source object to clone
     * @param <T> 泛型包装
     * @return a clone of the source object, or {@code null} if the source object is null or not an array
     */
    public static <T> T clone(final T source) {
        if (null == source) {
            return null;
        }
        if (isArray(source)) {
            final Object result;
            final Class<?> componentType = source.getClass().getComponentType();
            if (componentType.isPrimitive()) {
                int length = Array.getLength(source);
                result = Array.newInstance(componentType, length);
                while (length-- > 0) {
                    Array.set(result, length, Array.get(source, length));
                }
            } else {
                result = ((Object[]) source).clone();
            }
            return (T) result;
        }
        return null;
    }

    /**
     * Splits the given source byte array into smaller byte arrays of the specified length.
     * <p>
     * The last part may be shorter than the specified length if the source array length is not evenly divisible by the length.
     *
     * @param source the source byte array to split
     * @param len    the length of each part
     * @return a two-dimensional byte array containing the split parts
     */
    public static byte[][] split(byte[] source, int len) {
        int x = source.length / len;
        int y = source.length % len;
        int z = 0;
        if (y != 0) {
            z = 1;
        }
        byte[][] sources = new byte[x + z][];
        byte[] arr;
        for (int i = 0; i < x + z; i++) {
            arr = new byte[len];
            if (i == x + z - 1 && y != 0) {
                System.arraycopy(source, i * len, arr, 0, y);
            } else {
                System.arraycopy(source, i * len, arr, 0, len);
            }
            sources[i] = arr;
        }
        return sources;
    }

    /**
     * Zips the given keys and values arrays into a map.
     * <p>
     * The resulting map is not guaranteed to be in order. If the lengths of the keys and values arrays are not equal,
     * the shorter length is used.
     *
     * @param <K>    the type of the keys
     * @param <V>    the type of the values
     * @param keys   the keys array
     * @param values the values array
     * @return a map containing the key-value pairs from the keys and values arrays, or {@code null} if either array is empty
     */
    public static <K, V> Map<K, V> zip(K[] keys, V[] values) {
        return zip(keys, values, false);
    }

    /**
     * Zips the given keys and values arrays into a map.
     * <p>
     * If the {@code isOrder} parameter is {@code true}, the resulting map will be a {@link LinkedHashMap} and will maintain the order of the keys.
     * If the lengths of the keys and values arrays are not equal, the shorter length is used.
     *
     * @param <K>     the type of the keys
     * @param <V>     the type of the values
     * @param keys    the keys array
     * @param values  the values array
     * @param isOrder whether the resulting map should maintain the order of the keys
     * @return a map containing the key-value pairs from the keys and values arrays, or {@code null} if either array is empty
     */
    public static <K, V> Map<K, V> zip(K[] keys, V[] values, boolean isOrder) {
        if (isEmpty(keys) || isEmpty(values)) {
            return null;
        }

        final int size = Math.min(keys.length, values.length);
        final Map<K, V> map = newHashMap(size, isOrder);
        for (int i = 0; i < size; i++) {
            map.put(keys[i], values[i]);
        }

        return map;
    }

    /**
     * new a HashMap for array operation
     *
     * @param size the size of HashMap, because the load factory is 0.75
     *             so the actual size is size / 0.75
     * @param isOrder is the Map Key in order,
     *                true return {@link LinkedHashMap}, false return {@link HashMap}
     * @param <K> Key type
     * @param <V> Value type
     * @return the result HashMap
     */
    public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
        int initialCapacity = (int) (size / 0.75);
        return isOrder ? new LinkedHashMap<K, V>(initialCapacity) : new HashMap<K, V>(initialCapacity);
    }


    /**
     * get the index of source with input value, if not found return {@link #INDEX_NOT_FOUND}
     *
     * @param source source T source
     * @param dest the dest check object
     * @param <T> T array
     * @return the position of checker in source, if not found return {@link #INDEX_NOT_FOUND}
     */
    public static <T> int indexOf(T[] source, Object dest) {
        for (int i = 0; i < source.length; i++) {
            if(ObjectUtils.nullSafeEquals(dest, source[i])){
                return i;
            }
        }
        return INDEX_NOT_FOUND;
    }

    /**
     * get the last index of source with input value,
     * if not found return {@link #INDEX_NOT_FOUND}
     *
     * @param source source T source
     * @param dest the dest check object
     * @param <T> T array
     * @return the last position of checker in source, if not found return {@link #INDEX_NOT_FOUND}
     */
    public static <T> int lastIndexOf(T[] source, Object dest) {
        for (int i = source.length-1; i >= 0; i--) {
            if(ObjectUtils.nullSafeEquals(dest, source[i])){
                return i;
            }
        }
        return INDEX_NOT_FOUND;
    }

    /**
     * if the source T contains the dest T
     *
     * @param source source source
     * @param dest the dest
     * @param <T> 泛型包装
     * @return if contains true, else false
     */
    public static <T> boolean contains(T[] source, T dest) {
        return indexOf(source, dest) > INDEX_NOT_FOUND;
    }


    /**
     * wrap the source object to Object[] array
     *
     * @param source the source object, can be a object array or a base array
     * @return Object[] after wrap
     * @throws CoreException the source is not array
     */
    public static Object[] wrap(Object source) {
        if (isArray(source)) {
            try {
                return (Object[]) source;
            } catch (Exception e) {
            }
        }
        throw new CoreException(StringUtils.format("[{}] is not Array!", source.getClass()));
    }

    /**
     * if the source object is a Array
     *
     * @param source the source object
     * @return if is a array true, else false
     * @throws NullPointerException if the source is null, then<code>null</code>
     */
    public static boolean isArray(Object source) {
        if (null == source) {
            throw new NullPointerException("Object check for isArray is null");
        }
        return source.getClass().isArray();
    }

    /**
     * trans the source object to string
     *
     * @param source the source object
     * @return String the string of source object
     */
    public static String toString(Object source) {
        if (null == source) {
            return null;
        }
        if (ArrayUtils.isArray(source)) {
            try {
                return Arrays.deepToString((Object[]) source);
            } catch (Exception e) {
//                final String className = source.getClass().getComponentType().getName();
//                switch (className) {
//                    case "long":
//                        return Arrays.toString((long[]) source);
//                    case "int":
//                        return Arrays.toString((int[]) source);
//                    case "short":
//                        return Arrays.toString((short[]) source);
//                    case "char":
//                        return Arrays.toString((char[]) source);
//                    case "byte":
//                        return Arrays.toString((byte[]) source);
//                    case "boolean":
//                        return Arrays.toString((boolean[]) source);
//                    case "float":
//                        return Arrays.toString((float[]) source);
//                    case "double":
//                        return Arrays.toString((double[]) source);
//                    default:
//                        throw new CoreException(e);
//                }
            }
        }
        return source.toString();
    }

    /**
     * join the source T with input spt
     *
     * @param source source T source
     * @param spt separator
     * @param <T> 泛型包装
     * @return String the join string of source with spt
     */
    public static <T> String join(T[] source, CharSequence spt) {
        if (null == source) {
            return null;
        }

        final StringBuilder sb = new StringBuilder();
        boolean isFirst = true;
        for (T item : source) {
            if (isFirst) {
                isFirst = false;
            } else {
                sb.append(spt);
            }
            if (ArrayUtils.isArray(item)) {
                sb.append(join(ArrayUtils.wrap(item), spt));
            } else if (item instanceof Iterator<?>) {
                sb.append(CollectionUtils.join((Iterator<?>) item, spt));
            } else {
                sb.append(item);
            }
        }
        return sb.toString();
    }


    /**
     * trans {@link ByteBuffer} source to byte[]
     *
     * @param source the source byte buffer{@link ByteBuffer}
     * @return byte[] byte array
     * @since 1.6
     */
    public static byte[] toArray(ByteBuffer source) {
        if (false == source.hasArray()) {
            int oldPosition = source.position();
            source.position(0);
            int size = source.limit();
            byte[] buffers = new byte[size];
            source.get(buffers);
            source.position(oldPosition);
            return buffers;
        } else {
            return Arrays.copyOfRange(source.array(), source.position(), source.limit());
        }
    }

    /**
     * @param array the source byte buffer
     * @return the byte stream
     */
    public static byte[] toPrimitive(Byte[] array) {
        if (array == null) {
            return null;
        } else if (array.length == 0) {
            return EMPTY_BYTE_ARRAY;
        } else {
            byte[] result = new byte[array.length];

            for(int i = 0; i < array.length; ++i) {
                result[i] = array[i];
            }

            return result;
        }
    }


    /**
     * <p>Copies the given array and adds the given element at the end of the new array.
     *
     * <p>The new array contains the same elements of the input
     * array plus the given element in the last position. The component type of
     * the new array is the same as that of the input array.
     *
     * <p>If the input array is {@code null}, a new one element array is returned
     *  whose component type is the same as the element, unless the element itself is null,
     *  in which case the return type is Object[]
     *
     * <pre>
     * ArrayUtils.add(null, null)      = IllegalArgumentException
     * ArrayUtils.add(null, "a")       = ["a"]
     * ArrayUtils.add(["a"], null)     = ["a", null]
     * ArrayUtils.add(["a"], "b")      = ["a", "b"]
     * ArrayUtils.add(["a", "b"], "c") = ["a", "b", "c"]
     * </pre>
     *
     * @param <T> 泛型包装
     * @param array  the array to "add" the element to, may be {@code null}
     * @param element  the object to add, may be {@code null}
     * @return A new array containing the existing elements plus the new element
     * The returned array type will be that of the input array (unless null),
     * in which case it will have the same type as the element.
     * If both are null, an IllegalArgumentException is thrown
     * @since 2.1
     * @throws IllegalArgumentException if both arguments are null
     */
    public static <T> T[] add(final T[] array, final T element) {
        final Class<?> type;
        if (array != null) {
            type = array.getClass().getComponentType();
        } else if (element != null) {
            type = element.getClass();
        } else {
            throw new IllegalArgumentException("Arguments cannot both be null");
        }
        @SuppressWarnings("unchecked") // type must be T
        final
        T[] newArray = (T[]) copyArrayGrow1(array, type);
        newArray[newArray.length - 1] = element;
        return newArray;
    }

    /**
     * Returns a copy of the given array of size 1 greater than the argument.
     * The last value of the array is left to the default value.
     *
     * @param array The array to copy, must not be {@code null}.
     * @param newArrayComponentType If {@code array} is {@code null}, create a size 1 array of this type.
     * @return A new copy of the array of size 1 greater than the input.
     */
    private static Object copyArrayGrow1(final Object array, final Class<?> newArrayComponentType) {
        if (array != null) {
            final int arrayLength = Array.getLength(array);
            final Object newArray = Array.newInstance(array.getClass().getComponentType(), arrayLength + 1);
            System.arraycopy(array, 0, newArray, 0, arrayLength);
            return newArray;
        }
        return Array.newInstance(newArrayComponentType, 1);
    }

    /**
     * <p>Adds all the elements of the given arrays into a new array.
     * <p>The new array contains all of the element of {@code array1} followed
     * by all of the elements {@code array2}. When an array is returned, it is always
     * a new array.
     *
     * <pre>
     * ArrayUtils.addAll(null, null)     = null
     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
     * ArrayUtils.addAll([], [])         = []
     * ArrayUtils.addAll([null], [null]) = [null, null]
     * ArrayUtils.addAll(["a", "b", "c"], ["1", "2", "3"]) = ["a", "b", "c", "1", "2", "3"]
     * </pre>
     *
     * @param <T> 泛型包装
     * @param array1  the first array whose elements are added to the new array, may be {@code null}
     * @param array2  the second array whose elements are added to the new array, may be {@code null}
     * @return The new array, {@code null} if both arrays are {@code null}.
     *      The type of the new array is the type of the first array,
     *      unless the first array is null, in which case the type is the same as the second array.
     * @since 2.1
     * @throws IllegalArgumentException if the array types are incompatible
     */
    public static <T> T[] addAll(final T[] array1, @SuppressWarnings("unchecked") final T... array2) {
        if (array1 == null) {
            return clone(array2);
        } else if (array2 == null) {
            return clone(array1);
        }
        final Class<?> type1 = array1.getClass().getComponentType();
        @SuppressWarnings("unchecked") // OK, because array is of type T
        final T[] joinedArray = (T[]) Array.newInstance(type1, array1.length + array2.length);
        System.arraycopy(array1, 0, joinedArray, 0, array1.length);
        try {
            System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
        } catch (final ArrayStoreException ase) {
            // Check if problem was due to incompatible types
            /*
             * We do this here, rather than before the copy because:
             * - it would be a wasted check most of the time
             * - safer, in case check turns out to be too strict
             */
            final Class<?> type2 = array2.getClass().getComponentType();
            if (!type1.isAssignableFrom(type2)) {
                throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of "
                        + type1.getName(), ase);
            }
            throw ase; // No, so rethrow original
        }
        return joinedArray;
    }
}